<?php
/**
 * Gestion des templates du site
 * Analyse le template et remplace les variables là où il faut
 * ainsi que les objets etc selon le modèle du fichier template
 * Utilisation :
 * - Créer l'objet avec le fichier à charger
 * -- Dans le fichier template on trouve :
 * --- <@nom_de_template> : Template lié (comme un include)
 * --- <$nom_de_variable> : Variable à remplacer
 * --- <%nom_de_clef_de_localisation> : Clef de localisation
 * --- <node::nom_du_node></node::nom_du_node> : Noeud où effectuer des trucs
 * --- Tout le reste de l'html est laissé tel quel
 * - le fichier est découpé et placé dans un array $_TmpTemplate
 * -- L'array est découpé en sous tableaux pour chaque node présent
 * -- Chaque node est enregistré dans le tableau $_Nodes[nom] = path
 * - A partir de là on peu envoyer des tableaux de variables à remplacer
 *   dans un node précis avec la méthode geneNode
 * - La méthode display recrée dans un premier temps le template complet, puis
 *   Récupère les variable de localisation dans la langue correspondante
 *   à cette du navigateur client et en fin l'affiche avec un echo
 *
 * @author  Joseph Lemoine - lemoine.joseph@gmail.com
 * @version 3
 */

/* Le chemin par défaut du template est indiqué pour le front office, pour
 * l'utiliser dans d'auter ressources, il faut le spécifier AVANT la première
 * déclaration de la class
 */
if (!defined("TEMPLATE_PATH")) {
    define("TEMPLATE_PATH",
		ROOT."/www-frontoffice/theme/"
		. $_SESSION["theme"] . "/templates/");
}

class Template {

    /**
     * Langue par défaut à utiliser si la langue du navigateur n'est pas connue
     * @var	string
     */
    private $_lang = "FR";

    /**
     * template sous forme de tableau
     * @var	array
     */
    private $_tmpTemplate = array();

    /**
     * Adresse des nodes
     * @var	array
     */
    private $_nodePath = array();

    /**
     * Plugins à inssérer dans head
     * @var	string
     */
    private $_plugins = "";

    /**
     * Définie si la page ainsi générée est mise en cache
     * @var	bool
     */
    private $_cache = true;

    /**
     * Charge le tableau de node et leur adresse
     * @param	string	$file		Fichier de template à charger
	 * @param	bool	$renewCache	Autorise ou non la régénération du cache
     * @access	public
     */
    public function __construct($file, $renewCache = true) {
		/* Récupération du template */
		$this->_tmpTemplate["xml"] = $this->load($file, $renewCache);

		/* Enregistrement des nodes à utiliser */
		$this->registerNodes($this->_tmpTemplate);

		/* Seulement si on localise le site */
		if(LOCALISE)
			/* Recherche de la langue à utiliser */
			$this->setDefaultLang();
    }

    /**
     * Force la non mise en cache, méthode à appeler
     * avant affichage
     * @return $this
     */
    public function forbidCache() {
    	$this->_cache = false;
    	return $this;
    }

    /**
     * Charge le template et sous template
     * @param	string	$file	Fichier à charger
	 * @param	bool	$renewCache	Autorise ou non la régénération du cache
     * @access	private
     */
    private function load($file, $renewCache = true) {
		/* On recherche dans le cache si le fichier existe */
		$cacheName = TEMPLATE_PATH."cache/".(md5(print_r($_GET, 1).print_r($_POST, 1).$file)).".html";
		/* Si il existe */
		if(file_exists($cacheName)) {
			echo "Fichier trouvé";
			/* Le chache est-il encore à jour ? */
			if(time() - filemtime($cacheName) < CACHELIFE || !$renewCache){
				/* Il est à jour, on l'extrait et on affiche */
				$this->_tmpTemplate["xml"] = file_get_contents($cacheName);
				$this->display();
				exit();
			} else {
				/* Sinon il est obselète, on supprime */
				unlink($cacheName);
			}
		} else
			echo "Fichier non trouvé";
		
		/* Si le fichier existe bien */
		if(file_exists(TEMPLATE_PATH . $file . ".html")){
			$xml = file_get_contents(TEMPLATE_PATH . $file . ".html");

			/* Récupération des templates à inclure */
			$pattern = '#<@([^>]*)>#s';
			preg_match_all($pattern, $xml, $matches);

			/* Si on à bien des résultats */
			if (!empty($matches[1])) {
				/* Pour chaque sous template à inclure */
				foreach ($matches[1] AS $key => $fileName) {
					/* Chargement et remplacement dans le template parent */
					$xml = str_replace( "<@" . $fileName . ">", $this->load($fileName, $renewCache), $xml);
				}
			}
		} else {
			echo "TEMPLATE " . TEMPLATE_PATH . $file . ".html INCONNUE !!!";
			exit();
		}

		/* Si on à chargé un fichier */
		if(!empty($xml)) {
			/* Récupération des plugins */
			$pattern = '#<plug>(.*?)<\/plug>#s';
			preg_match_all($pattern, $xml, $matches);

			/* Si on à bien des résultats */
			if (!empty($matches[1])) {
			/* Pour chaque plugins à ajouter */
				foreach ($matches[1] AS $key => $plugin) {
					/* Mise en tampon */
					$this->_plugins .= $plugin;
					/* Suppression de la balise */
					$xml = str_replace( "<plug>".$plugin."</plug>", "", $xml);
				}
			}
		}

		return $xml;
    }

    /**
     * Découpe le XML en tableaux de node etc ...
     * @access	private
     */
    private function registerNodes(&$node) {
		/* Purge des caractères de retour chariot pour pas bousiller les inline-block */
		$node["xml"] = str_replace(array("\t", "\r", "\n"), "", $node["xml"]);
		/* Ajout des plugins */
		$this->addPlugins($node["xml"]);

		/* Récupération des noeuds à traiter */
		$pattern = '#<node::([^>]*)>(.*?)<\/node::\1>#s';
		preg_match_all($pattern, $node["xml"], $matches);

		/* Si on à bien des nodes */
		if (!empty($matches[1])) {

		    /* Ajout du contenue pour chaque node trouvé */
		    foreach ($matches[1] AS $key => $nodeName) {
			/* Ajout du contenue */
			$node["node"][$nodeName]["xml"] = $matches[2][$key];

			/* On remplace le contenue du node par un flag */
			$node["xml"] = str_replace($matches[0][$key], "<flag::" . $nodeName . ">", $node["xml"]);

			/* Enregistrement des nodes enfants */
			$this->registerNodes($node["node"][$nodeName]);
		    }

		}

		/* Enregistrement des variables */
		$pattern = '#<\$([^>]*)>#s';
		preg_match_all($pattern, $node["xml"], $matches);

		/* Si on à bien des variables */
		if (!empty($matches[1])) {
		    /* Ajout de la variable dans le node */
		    foreach ($matches[1] AS $key => $varName) {
			$node["var"][$varName] = "";
		    }
		}
    }

    /**
     * Attribue une valeur à une variable. Si aucun node n'est précisé,
     * on commence l'attribution à la racine
     * @param	string	$varName    Nom de la variable
     * @param	mixed	$varValue   Valeur de la variable
     * @param	bool	$cascade    Remplacer dans les sous nodes ou non
     * @param	&	$node	    Référence vers le node à scanner, attention
     *				    ce paramètre ne doit-être passé que par
     *				    récursion ou un alias de cette methode
     * @access	public
     */
    public function setVar($varName, $varValue, $cascade = false, &$node = null) {
		/* Si on à pas passé de référence de node*/
		if (empty($node)) {$node = &$this->findNode("");}

		/* Traitement des sous node */
		if (!empty($node["node"]) && $cascade) {
		    /* Les références ne suivent pas $osef donc on s'en sert pas */
		    foreach ($node["node"] AS $nodeName => $osef) {
			/* On applique aux sous node */
			$this->setVar($varName, $varValue, $cascade, $node["node"][$nodeName]);
		    }
		}

		/* Si la variable est présente dans le node, on lui donne sa valeur */
		if (isset($node["var"][$varName])) {$node["var"][$varName] = $varValue;}
    }

    /**
     * Alias de setVar avec argument pour préciser le nom du node où remplacer
     * @param	string	$varName    Nom de la variable
     * @param	mixed	$varValue   Valeur de la variable
     * @param	bool	$cascade    Remplacer dans les sous nodes ou non
     * @param	string	$nodeName   Nom du node
     * @access	public
     */
    public function setVarToNode($varName, $varValue, $cascade = false, $nodeName = null) {
		/* Recherche du node */
		$node = &$this->findNode($nodeName);
		/* Enregistrement de sa valeur */
		$this->setVar($varName, $varValue, $cascade, $node);
    }

    /**
     * Attribue un tableau de variable à un node
     * @param	array	$varList    [$varName] = $varValue
     * @param	bool	$cascade    Remplacer dans les sous nodes ou non
     * @param	string	$nodeName   Nom du node où remplacer tout ça
     * @access	public
     */
    public function setArrayToNode($varList, $cascade = false, $nodeName = null) {
		/* Recherche du node */
		$node = &$this->findNode($nodeName);

		/* enregistrement de toute les variables */
		foreach ($varList AS $varName => $varValue) {
		    $this->setVar($varName, $varValue, $cascade, $node);
		}
    }

    /**
     * Appel et traite automatiquement une requète SQL pour l'intégrer au
     * node spécifié. En cas de ligne de résulta multiple, les node
     * sont copiés automatiquement.
     * @param	$query		string	Requète SQL
     * @param	$nodeName	string	Nom du node où isérer les résultats
     * @param	$alias		string	Alias de la base de donnée
     * @return	bool
     * @access	public
     */
    public function autoAnalyse($query, $nodeName = null, $alias = null) {
    	/* Si ce n'est pas une requète vide */
    	if(!empty($query)) {
    		/* Exécution de la requète SQL */
    		$req = Sql::run($query, $alias);
    		/* Si le résulta n'est pas vide */
    		if(!empty($req)) {
    			/* Destruction du node de vide */
    			$this->killNode("no".$nodeName);

    			/* Traitement de la requète */
    			foreach($req AS $row => $data) {
    				/* Initialisation */
    				$varList = array();
    				/* Les variables récupérés sont sensés être correctement només */
    				foreach($data AS $colName => $rowVal) {
    					/* Les nom de variables sont des string */
    					if(is_string($colName)) {
    						$varList[$colName] = $rowVal;
    					}
    				}

    				/* Ajout au template */
    				$this->setArrayToNode($varList, true, $nodeName);

    				/* Copie du node si il est spécifié */
    				$this->copyNode($nodeName);
    			}
    			/* Destruction de la dernière copie */
    			$this->killNode($nodeName);
    			return true;
    		} else {
    			/* Destruction du node */
    			$this->killNode($nodeName);
    			return false;
    		}
    	} else {
    		/* Destruction du node */
    		$this->killNode($nodeName);
    		return false;
    	}
    }

    /**
     * Duplique un node
     * @param	string	$nodeName   Nom du node à dupliquer
     * @access	public
     */
    public function copyNode($nodeName) {
		/* recherche du parent du node */
		$parent = &$this->findParentNode($nodeName);

		if(!empty($parent)){
		    /* Si le parent n'est pas finalisé, on le finalise */
		    if(empty($parent["final"])) {$parent["final"] = $parent["xml"];}

		    /* Rebuild du node à dupliquer */
		    $this->rebuildXML($parent["node"][$nodeName]);

		    /* Ajout des données du node à dupliquer dans son parent et rajout du flag */
		    $parent["final"] = str_replace(
			"<flag::" . $nodeName . ">",
			$parent["node"][$nodeName]["final"] . "<flag::" . $nodeName . ">",
			$parent["final"]);

		    /* Destruction de la finalisation du node */
		    unset($parent["node"][$nodeName]["final"]);
		} else {
		     /* Pas de parent, on renvoit faux */
		    return FALSE;
		}
    }

    /**
     * Supprime l'adressage d'un noeud dans son parent
     * @param	string	$nodeName   Nom du node à détruire
     * @access	public
     */
    public function killNode($nodeName) {
		/* recherche du parent du node */
		$parent = &$this->findParentNode($nodeName);

		if(!empty($parent)){
		     /* Si le parent n'est pas finalisé, on le finalise */
		    if(empty($parent["final"])) {$parent["final"] = $parent["xml"];}

		    /* supression des données du node dans son parent */
		    $parent["final"] = str_replace("<flag::" . $nodeName . ">", "", $parent["final"]);
		}
    }

    /**
     * Trouve et retourne le node avec le nom spécifié
     * @param	string	$searchName Nom du node à trouver
     * @param	array	$node	    Node de référence pour débuter la recherche
     * @return	&		    pointeur vers le node correspondant
     * @access	private
     */
    private function &findNode($searchName, &$node = NULL) {
		/* Si $searchName est vide ou vaut "root" on renvoit la racine */
		if (empty($searchName) || strtolower($searchName) == "root") {
		    $node = &$this->_tmpTemplate;
		    return $node;
		}

		/**
		 * Si on spécifier un node de référence on le passe en paramètre,
		 * sinon on part de la racine
		 */
		if(empty($node))
		    $node = &$this->_tmpTemplate;

		/* Si la racine donnée à bien des sous node */
		if(is_array($node["node"]))
		    /* Parcourt de l'arboressence */
		    foreach ($node["node"] AS $nodeName => $nodeDetails) {

		/* Si c'est le bon, on le renvoit directement */
		if ($searchName == $nodeName)
		    $result = &$node["node"][$nodeName];
		/* Sinon si il y a des sous nodes, on les analyses */
		else if (isset($nodeDetails["node"]))
		    $result = &$this->findNode($searchName, $node["node"][$nodeName]);

		/* Si on à un résultat, on stop la recherche */
		if (!empty($result))
		    break;
	    }

		/* On renvoit le résultat */
		return $result;
    }

    /**
     * Trouve et retourne le node avec le nom spécifié
     * @param	string	$searchName Nom du node à trouver
     * @param	array	$node	    Node de référence pour débuter la recherche7
     * @return	&		    pointeur vers le node correspondant
     * @access	private
     */
    private function &findParentNode($searchName, &$node = NULL) {
		/**
		 * Si on spécifier un node de référence on le passe en paramètre,
		 * sinon on part de la racine
		 */
		if(empty($node))
		    $node = &$this->_tmpTemplate;

		/* Si la racine donnée à bien des sous node */
		if(is_array($node["node"]))
		    /* Parcourt de l'arboressence */
		    foreach ($node["node"] AS $nodeName => $nodeDetails) {

			/* Si c'est le bon, on le renvoit directement */
			if ($searchName == $nodeName)
			    $result = &$node;
			/* Sinon si il y a des sous nodes, on les analyses */
			else if (isset($nodeDetails["node"]))
			    $result = &$this->findParentNode($searchName, $node["node"][$nodeName]);

			/* Si on à un résultat, on stop la recherche */
			if (!empty($result))
			    break;
		    }

		/* On renvoit le résultat */
		return $result;
    }

    /**
     * Recrée l'arboressence du XML
     * @param	array	$node	node à recréer
     * @access	private
     */
    private function rebuildXML(&$node) {

		/* On finalise notre node si c'est pas déjà fait */
		if(empty($node["final"])) {$node["final"] = $node["xml"];}

		/* Si on à des sous node */
		if (isset($node["node"])) {
		    /* Les références ne suivent pas $osef donc on s'en sert pas */
		    foreach ($node["node"] AS $nodeName => $osef) {

			/* on finalise les sous nodes */
			$this->rebuildXML($node["node"][$nodeName]);

			/* On remplace les flags du node par leur valeur */
			$node["final"] = str_replace(
				"<flag::" . $nodeName . ">",
				$node["node"][$nodeName]["final"],
				$node["final"]);

			/* On détuit la finalisation du sous node */
			unset($node["node"][$nodeName]["final"]);
		    }
		}

		/* Ecriture des variables */
		if (!empty($node["var"])) {
		    foreach ($node["var"] AS $varName => $varValue) {
			/* Inscription des valeurs */
			$node["final"] = str_replace("<$" . $varName . ">", $varValue, $node["final"]);
			/* Réinitialisation de la variable */
			$node["var"][$varName] = "";
		    }
		}
    }

    /**
     * Touve et remplace toutes les occurences de localisation dans le XML
     * envoyé en argument
     * @param	string	$xml	xml où appliquer la localisation
     * @access	private
     */
    private function localiseXML(&$xml) {

		/* Recherche des clef de localisation */
		$pattern = '#<%([^>]*)>#s';
		preg_match_all($pattern, $xml, $matches);

		/* Si on à des clefs à traiter */
		if (!empty($matches[1])) {
		    /* Enregistrement de chaque clef */
		    foreach ($matches[1] AS $n => $keyName) {
			/* Récuépration de la valeur par DAO */
			$key = new LangLocalised($keyName);

			/* Définition de la valeur par défaut de la clef si elle n'existe pas */
			if(empty($key->id)) {
				if(DEBUG) {
					$replace = "<span class='triggerLoc'>"
						."<a href='http://localhost/folio/www-backoffice/editlockey.php?key=".$keyName."' target='_blanc'>".$keyName."</a>"
						."</span>";
				} else {
					$replace = $keyName;
				}
			} else if(EDIT_KEY) {
				$replace = "<textarea name='".$keyName."' class='ultraEdit'>"
					.$key->content
					."</textarea>";
			} else
			    $replace = $key->content;

			$xml = str_replace($matches[0][$n], $replace, $xml);
		    }
		}

    }

    /**
     * Définie la langue à utiliser
     * @access	private
     */
    private function setDefaultLang() {

		/* Si on à pas déjà créer de session spéciale */
		if(empty($_SESSION["lang"])){
		    /**
		     * Récupération et test de la langue du navigateur
		     * Si elle est invalide on utilise $_lang qui est définie par
		     * défault dans une langue valide
		     */
		    $tempLang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
		    if(Template::testLang($tempLang)){
				/* On définie cette langue comme langue d'affichage */
				$this->_lang = $tempLang;
		    }

		    /* On sauvegarde cette langue comme langue de session */
		    $_SESSION["lang"] = $this->_lang;

		/* Sinon si on à déjà une langue en session */
		} else {
		    /* C'est qu'on l'à définie par ajax, donc on l'utilise */
		    $this->_lang = $_SESSION["lang"];
		}
    }

    /**
     * Test si une langue donnée existe dans les localisations du site
     * @param	string	$tag	Tag de la langue à tester
     * @access	public
     * @static
     */
    public static function testLang($tag) {
		/* On empèche les injections sql en coupant à juste 2 caractères */

		/* Avons nous cette langue en base de donnée ? */
		$sql = "SELECT
			s.id
		    FROM langsupport AS s
		    WHERE s.tag = '" . $tag . "'";

		/* Exécution */
		$req = Sql::run($sql);

		/* Si on à bien une entrée dans la table des langues supportés */
		if(!empty($req)){
		    return TRUE;
		} else {
		    return FALSE;
		}
    }

    /**
     * Ajoute les plugins enregistrés dans les différents XML chargés
     * @param	&		$xml	    Référence de la chaine à traiter
     * @access	private
     */
    private function addPlugins(&$xml) {
		/* Recherche des balises head */
		$pattern = '#<head>(.*?)<\/head>#s';
		preg_match_all($pattern, $xml, $matches);

		/* Si on à bien des résultats */
		if (!empty($matches[1])) {
		    /* Pour chaque head (normalement un seul) */
		    foreach ($matches[1] AS $key => $head) {
				/* replacement de head */
				$xml = str_replace($matches[0][$key], $head.$this->_plugins, $xml);
		    }
		}
    }

    /**
     * Vide le cache du thème
     * @access public
     */
    public static function flushCache() {
    	/* Si on à bien un dossier de cache */
    	if(is_dir(TEMPLATE_PATH."/cache/")) {
    		$folder = opendir(TEMPLATE_PATH."/cache/");

	    	while ($file = readdir($folder)) {
				if ($file != "." && $file != "..") {
					/* Suppression */
					unlink($folder.$file);
				}
			}
	        closedir($folder);
    	}
    }

    /**
     * Régénère le token
     * @access	public
     */
    public static function token() {
		$_SESSION["token"] = md5(uniqid());
    }

    /**
     * Génère l'html complet puis récupère les clefs de localisation à charger
     * et enfin affiche la page
     * @param	bool	$return	    Renvoit le template sous forme de string
     * @access	public
     */
    public function display($return = false) {
		$this->rebuildXML($this->_tmpTemplate);

		/* Seulement si on localise le site */
		if(LOCALISE)
			$this->localiseXML($this->_tmpTemplate["final"]);

		/* Inscription du cache le cas échéant */
		if($this->_cache && !file_exists(TEMPLATE_PATH."cache/".(md5(print_r($_GET, 1).print_r($_POST, 1).$file)).".html")) {
			$cache = fopen(TEMPLATE_PATH."cache/".(md5(print_r($_GET, 1).print_r($_POST, 1).$file)).".html","a");
			fputs($cache, $this->_tmpTemplate["final"]);
			fclose($cache);
		}

		if($return)
		    return $this->_tmpTemplate["final"];
		else
		    echo $this->_tmpTemplate["final"];
	}
}
?>